/*
 * Project: Sudoku Explainer
 * Copyright (C) 2006-2007 Nicolas Juillerat
 * 
 * Project: Sudoku Hint Engine for Android
 * Copyright (C) 2010-2013 Emanuel Moecklin
 * 
 * Available under the terms of the Lesser General Public License (LGPL)
 */
package com.onegravity.sudoku.solver.checks;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import com.onegravity.sudoku.solver.HintResource;
import com.onegravity.sudoku.solver.accumulator.FinalHintInterceptor;
import com.onegravity.sudoku.solver.accumulator.HintsAccumulator;
import com.onegravity.sudoku.solver.accumulator.SingleHintAccumulator;
import com.onegravity.sudoku.solver.accumulator.StopHintProductionException;
import com.onegravity.sudoku.solver.core.FinalHintProducer;
import com.onegravity.sudoku.solver.core.Hint;
import com.onegravity.sudoku.solver.core.HintProducer;
import com.onegravity.sudoku.solver.core.Rule;
import com.onegravity.sudoku.solver.core.WarningHintProducer;
import com.onegravity.sudoku.solver.data.Grid;

/**
 * This producer analyzes a puzzle and will produce one of the following hints (and only one):
 *  
 * - AnalysisResult -> AnalysisResult: analysis successfully performed
 * - AnalysisResult -> AnalysisFailed: analysis could not find a solution with the available solving techniques
 * - AnalysisResult -> DifficultyBeyondNightmare: puzzle is too difficult to analyze (performance)
 * - IncorrectDigitHint: incorrect digits found
 * - IncorrectPencilMarksHint: incorrect pencil marks found
 * - TooFewGivensHint: puzzle doesn't have enough givens
 * - NoSolutionHint: puzzle has no solution
 * - MultipleSolutionsHint: puzzle has multiple solutions
 * 
 * @author Emanuel Moecklin
 */
public class Analyzer implements WarningHintProducer {

	transient private List<HintProducer> hintProducers;
	
	private boolean isBeyondNightmare;

    public Analyzer(List<HintProducer> hintProducers, boolean isBeyondNightmare) {
		this.hintProducers = hintProducers;
		this.isBeyondNightmare = isBeyondNightmare;
	}
	
    @Override
	public void getHints(Grid grid, HintsAccumulator accu) throws StopHintProductionException, InterruptedException {
    	if (isBeyondNightmare) {
    		// if the difficulty level is beyond nightmare then don't do the analysis
        	accu.add(new AnalysisResult(grid, null, false, true));
    	}
    	else {
        	Grid copy = grid.clone();
            try {
            	// run analysis
        		Map<Rule,Integer> rules = analyze(grid);
        		// analysis succcessful
                Hint hint = new AnalysisResult(grid, rules, false, false);
                accu.add(hint);
            }
            catch (AnalysisException canHappen) {
            	if (canHappen.getType()==AnalysisException.Type.ANALYSIS_FAILED) {
            		// analysis failed
                    accu.add(new AnalysisResult(grid, null, true, false));
            	}
            	else {
            		// final hint will be shown
                    accu.add(canHappen.getHint());
            	}
            }
            finally {
            	// restore original state
                copy.copyTo(grid);
            }
    	}
    }

    @Override
    public String toString() {
        return HintResource.ANALYSIS.toString();
    }

    /**
     * Solves the Sudoku passed to the constructor.
     * <p>
     * Returns a sorted map between the rules that were used and their frequency.
     * Rules are sorted by difficulty.
     * @return the map between used rules and their frequency
     * @throws AnalysisException if for some reason the Sudoku can't be solved (solving techniques not sufficient, puzzle invalid etc.)
     * @throws InterruptedException if the production of hints should be stopped
     */
    private Map<Rule,Integer> analyze(Grid grid) throws AnalysisException, InterruptedException {
        Map<Rule,Integer> usedRules = new TreeMap<Rule,Integer>(new RuleComparator());

        // we want to collect these hint producers just once!
        boolean runFinalHintProducers = true;
        
        while (!isSolved(grid)) {
            SingleHintAccumulator accu = new SingleHintAccumulator();
            FinalHintInterceptor interceptor = new FinalHintInterceptor(accu);
            try {
                for (HintProducer producer : hintProducers) {
                	if (runFinalHintProducers || !(producer instanceof FinalHintProducer)) {
                        producer.getHints(grid, interceptor);
                	}
                    if (Thread.interrupted()) throw new InterruptedException();
                }
            }
            catch (StopHintProductionException mayHappen) {}

        	if (!interceptor.hasHints()) {
                // no hints found --> throw AnalysisException
				throw new AnalysisException(AnalysisException.Type.ANALYSIS_FAILED, null);
        	}

            Hint hint = accu.getHint();
        	if (!(hint instanceof MissingPencilMarksHint)) {
        		// MissingPencilMarksHint are not collected but just applied
        		if (interceptor.hasFinalHint()) {
                    // puzzle is somehow not correct --> throw AnalysisException
    				throw new AnalysisException(AnalysisException.Type.INVALID_PUZZLE, accu.getHint());
                }
            	else {
    	            // valid solver rule
    	            Rule rule = (Rule) hint;
    	            if (usedRules.containsKey(rule)) {
    	                usedRules.put(rule, usedRules.get(rule) + 1);
    	            }
    	            else {
    	                usedRules.put(rule, 1);
    	            }
            	}
        	}
            hint.apply();
            runFinalHintProducers = false;
        }
        
        return usedRules;
    }
    
    private boolean isSolved(Grid grid) {
        for (int y=0; y<9; y++) {
            for (int x=0; x<9; x++) {
                if (grid.getCellValue(x, y)==0) return false;
            }
        }
        return true;
    }

	private class RuleComparator implements Comparator<Rule> {
		public int compare(Rule r1, Rule r2) {
            double d1 = r1.getDifficulty();
            double d2 = r2.getDifficulty();
            if (d1 < d2) return -1;
            else if (d1 > d2) return 1;
            else return r1.getName().compareTo(r2.getName());
        }
    }

    @Override
	public String getProducerName() {
		return null;
	}
}